package edu.pdx.cs.cs542s07.minichess;

import java.util.ArrayList;
import java.util.HashMap;

/**
 * A class to handle the basic functionality of the board.
 * This is for the term project for CS 542 -- Combinatorial Games
 * Taught by Bart Massey during the Spring Quarter of 2007 at Portland State.
 * @author Mike Smith <maxomai at gmail dot com>
 */
class Board {

	// 
	// STATIC MEMBERS
	//
	
	static final int NUMBER_OF_ROWS = 6;
	static final int NUMBER_OF_COLUMNS = 5;
	
	
	//
	// MEMBERS
	//
	
	/**
	 * A boolean deciding whether the game is over or not.
	 */
	private boolean gameOver;
	
	/**
	 * A boolean deciding whether someone has resigned or not.
	 */
	private boolean resigned = false;
	
	/** 
	 * A hashmap containing the states of all white pieces on the board. Captured pieces are expected to be removed.
	 */
	private HashMap<Piece, Square> whitePieces;
	
	/** 
	 * A hashmap containing the states of all black pieces on the board. Captured pieces are expected to be removed.
	 */
	private HashMap<Piece, Square> blackPieces;
	
	/** 
	 * A hashmap containing the states of all pieces on the board. Captured pieces are expected to be removed.
	 */
	private HashMap<Piece, Square> pieces;
	
	/** 
	 * A matrix representing the state of the board. board[i][j] represents the contents of the ith row and
	 * the jth column of the board (starting with 0, 0 at the lower left hand corner). If the contents are
	 * null then that position is assumed to be empty.
	 */
	private Piece[][] board;

	private static class PerformedMove {
		
		Move move;
		Piece capturedPiece = null;
		boolean promotedPawn = false;

	}
	
	/**
	 * A stack of moves performed.
	 */
	private ArrayList<PerformedMove> performedMoves;
	
	/**
	 * How many moves have occurred. A move is two "plys," 
	 * one for white, followed by one for black.
	 */
	private int moves;
	
	/**
	 * Whose turn it is. This should be 'B' for black and 'W' for white.
	 */
	private Color turn;
	
	/**
	 * The hash value for the board's state. This must be updated every time we move a piece
	 * or capture a piece.
	 */
	private int hash = HashFactory.STARTING_BOARD_VALUE;
	
	/**
	 * The board has its own hashfactory.
	 * @return
	 */
	private HashFactory hashfac = new HashFactory();
	
	/**
	 * The present score for the white player
	 * @return
	 */
	private int score = 0;
	
	//
	// GET METHODS
	//
	
	Piece[][] getBoard() {
		return board;
	}

	int getMoves() {
		return moves;
	}

	Color getTurn() {
		return this.turn;
	}
	
	HashMap<Piece, Square> getBlackPieces() {
		return blackPieces;
	}

	HashMap<Piece, Square> getPieces() {
		return pieces;
	}

	HashMap<Piece, Square> getWhitePieces() {
		return whitePieces;
	}
	
	boolean isResigned() {
		return resigned;
	}
	
	int getHash() {
		return hash;
	}
	
	int getScore() {
		int myscore;
		
		if (this.gameOver)
			myscore = ScoreFactory.OPPONENT_WINS;
		else 
			myscore = this.score;
		
		if (this.turn == Color.BLACK) 
			myscore *= -1;
		
		return myscore;
	}
	

	
	//
	// BUSINESS LOGIC METHODS
	//

	/** 
	 * Clears the chess board
	 */
	private void clear() {
		this.board = new Piece[NUMBER_OF_ROWS][NUMBER_OF_COLUMNS];
		this.whitePieces  = new HashMap<Piece, Square>();
		this.blackPieces  = new HashMap<Piece, Square>();
		this.pieces  = new HashMap<Piece, Square>();
		this.performedMoves = new ArrayList<PerformedMove>();
		this.turn = turn;
		this.score = 0;
		this.hash = HashFactory.STARTING_BOARD_VALUE;
	}
	
	/**
	 * Instantiates and initializes the chessboard.
	 */
	Board() {
		this.initialize();
	}
	
	/**
	 * Initializes from a board state string.
	 */
	Board(String stateString) 
	throws IllegalBoardStringException {
		this.fromString(stateString);
		this.gameOver = false;
	}

	/**
	 * Creates a deep, disposable copy of the current board. Necessary for iterative deepening.
	 */
	Board copy() {
		Board newBoard = new Board();
		newBoard.board = new Piece[NUMBER_OF_ROWS][]; 
		for (int index = 0; index < NUMBER_OF_ROWS; index++) {
			newBoard.board[index] = board[index].clone();
		}
		newBoard.whitePieces = (HashMap)whitePieces.clone();
		newBoard.blackPieces = (HashMap)blackPieces.clone();
		newBoard.pieces = (HashMap)pieces.clone();
		newBoard.moves = moves;
		newBoard.turn = turn;
		newBoard.hash = hash;
		newBoard.performedMoves = (ArrayList)performedMoves.clone();
		newBoard.score = score;
		return newBoard;
	}
	
	/**
	 * A helper function for creating a new game. 
	 * @param piece
	 * @param state
	 */
	private void init_helper(Piece piece, Square square) {
		this.pieces.put(piece, square);
		if (piece.color == Color.WHITE) {
			this.whitePieces.put(piece, square);
		} else {
			this.blackPieces.put(piece, square);
		}
		this.board[square.row][square.col] = piece;
	}
	
	/**
	 * Initializes the game state to a new game.
	 */
	private void initialize() {
		
		// clear the board
		this.clear();
		
		// set to first move
		this.moves = 1;
		this.turn = Color.WHITE;
		this.gameOver = false;
		
		// method variables
		Piece piece;
		Square square;
		
		// White king
		piece = new Piece(PieceType.KING, Color.WHITE);
		square = new Square(0, 4);
		init_helper(piece, square);

		// White queen
		piece = new Piece(PieceType.QUEEN, Color.WHITE);
		square = new Square(0, 3);
		init_helper(piece, square);		

		// White bishop
		piece = new Piece(PieceType.BISHOP, Color.WHITE);
		square = new Square(0, 2);
		init_helper(piece, square);
		
		// White knight
		piece = new Piece(PieceType.KNIGHT, Color.WHITE);
		square = new Square(0, 1);
		init_helper(piece, square);
		
		// White rook
		piece = new Piece(PieceType.ROOK, Color.WHITE);
		square = new Square(0, 0);
		init_helper(piece, square);
		
		// White pawns
		for (int index = 0; index <= 4; index++) {
			piece = new Piece(PieceType.PAWN, Color.WHITE);
			square = new Square(1, index);
			init_helper(piece, square);			
		}

		// Black king
		piece = new Piece(PieceType.KING, Color.BLACK);
		square = new Square(5, 0);
		init_helper(piece, square);

		// Black queen
		piece = new Piece(PieceType.QUEEN, Color.BLACK);
		square = new Square(5, 1);
		init_helper(piece, square);		

		// Black bishop
		piece = new Piece(PieceType.BISHOP, Color.BLACK);
		square = new Square(5, 2);
		init_helper(piece, square);
		
		// Black knight
		piece = new Piece(PieceType.KNIGHT, Color.BLACK);
		square = new Square(5, 3);
		init_helper(piece, square);
		
		// Black rook
		piece = new Piece(PieceType.ROOK, Color.BLACK);
		square = new Square(5, 4);
		init_helper(piece, square);
		
		// Black pawns
		for (int index = 0; index <= 4; index++) {
			piece = new Piece(PieceType.PAWN, Color.BLACK);
			square = new Square(4, index);
			init_helper(piece, square);			
		}		
		
		// the rest of the board
		for (int index = 0; index <= 4; index++) {
			this.board[2][index] = null;
			this.board[3][index] = null;
		}		
		
	} // end initialize()
		

	/** 
	 * Generate a board from a string. This needs to be modified and 
	 * checked carefully if the geometry of the board changes. 
	 * @param s The string to be converted
	 */
	void fromString(String s) 
		throws IllegalBoardStringException {
		// clear everything
		this.clear();
		
		// tokenize representation string
		String[] tokens = s.split("\\s");
		// sanity check
		if (tokens.length != 8) {
			throw new IllegalBoardStringException("Invalid board state string: too many tokens. " +
					"Check for unwanted white space.\n Input string:\n" + s + "\n");
		}
		
		// initialize moves
		this.moves 	= Integer.parseInt(tokens[0]);
		char turnToken	= tokens[1].charAt(0);
		// sanity check
		if (! (turnToken == 'B' || turnToken == 'W') ) {
			throw new IllegalBoardStringException("Invalid board state string: side needs to be 'B' or 'W' "
					+ "\n Input string:\n" + s + "\n");
		} else {
			if (turnToken == 'W') 
				this.turn = Color.WHITE;
			else
				this.turn = Color.BLACK;
		}
		
		// initialize board and pieceStates
		for (int index = 2; index < 8; index++) {
			String boardRow = tokens[index];
			// sanity check
			if (boardRow.length() != 5) {
				throw new IllegalBoardStringException(
						"Row " + (7-index) + " of board is the wrong length." 
						+ "\n Input string:\n" + s + "\n");
			}
			
			// parse each token
			for (int charcol = 0; charcol < 5; charcol++) {
				
				// Convert to piece
				char c = boardRow.charAt(charcol);
				Piece piece;
				try {
					piece = Piece.charToPiece(c);
				} catch (BadCharacterException e) {
					String message = "Character '" + c + "' at (" 
									+ (7-index) + "," + charcol + ") is invalid."
									+ "\n Input string:\n" + s + "\n";
					throw new IllegalBoardStringException(message);
				}
				
				// populate the board
				if(piece != null) {	
					// populate the state hashmap
					Square pos = new Square(7-index, charcol);														
					init_helper(piece, pos);
				} 
				
			} // for
			
		} // for

	} // end fromString()

	/**
	 * Prints the state of the board as a string.
	 */
	public String toString() {
		StringBuffer buff = new StringBuffer();
		Piece piece; 
		buff.append(this.moves);
		buff.append(' ');
		if (this.turn == Color.WHITE)
			buff.append('W');
		else
			buff.append('B');
		buff.append('\n');
		for (int row = NUMBER_OF_ROWS-1; row >= 0; row--) {
			for (int col = 0; col <= NUMBER_OF_COLUMNS-1; col++) {
				piece = this.board[row][col];
				if (piece == null) {
					buff.append('.');
				} else {
					buff.append(piece.toChar());
				}
			}
			buff.append('\n');
		}
		return buff.toString();
	} 

	static boolean promotePawn(Piece pawn, Square moveTo) {
		 return (pawn.color == Color.BLACK && moveTo.row == 0) ||
		 		(pawn.color == Color.WHITE && moveTo.row == NUMBER_OF_ROWS-1);
	}
	
	/**
	 * Executes a move
	 */
	Board move(Move m) throws IllegalMoveException {
		moveHelper(m, this);
		return this;
	}
	
	/**
	 * Simulates a move
	 */
//	Board simulatesMove(Move m) throws IllegalMoveException {
//		Board newBoard = new Board(this.board, this.whitePieces, this.blackPieces,
//									this.pieces, this.moves, this.turn);
//		moveHelper(m, newBoard);
//		return newBoard;
//	}

	/**
	 * Helps execute a move.
	 * @param m
	 */
	private static void moveHelper(Move m, Board b) throws IllegalMoveException {
		
		// handle resignation case
		if (m.isResign()) {
			b.gameOver = true;
			b.resigned = true;
			return;
		}
		
		Square from 		= m.getFrom();
		Square to 			= m.getTo();
		Piece movedPiece 	= b.board[from.row][from.col];
		PerformedMove performedMove = new PerformedMove();
		
		// 	check that game is still on.
		if (b.gameOver) {
			throw new IllegalMoveException("The game is over.");
		}
		
		//		 check that from contains a legitimate piece
		if (movedPiece == null) {
			throw new IllegalMoveException(from.toString() + " does not contain a piece.");
		}
		if (movedPiece.color != b.turn) {
			throw new IllegalMoveException(from.toString() + " does not contain a piece of your color.");
		}
		
		// check that the piece can be moved.
		MoveFactory fac = new MoveFactory();
		ArrayList<Move> moves = fac.generateMovesForPiece(b, movedPiece, from);
		if (moves.isEmpty()) {
			throw new IllegalMoveException(movedPiece.color + " " +  movedPiece.type + " at " 
					+ from.toString() + " cannot be moved.");
		}
		
		// check that the move is a legitimate move.
		boolean isInList = false;
		for (Move possibleMove: moves) {
			if (m.equals(possibleMove)) {
				isInList = true;
				break;
			}
		}
		if (!isInList) {
			throw new IllegalMoveException(movedPiece.color + " " +  movedPiece.type + " at " + 
					from.toString() + " cannot be moved to " + to.toString());
		}
		
		
		// check to square; if there's a piece there, capture it.
		Piece capturedPiece = b.board[to.row][to.col];
		if (capturedPiece != null) {
			
			b.removePiece(capturedPiece, to);
			performedMove.capturedPiece = capturedPiece;
			
			if (capturedPiece.type == PieceType.KING) {
				b.gameOver = true;
			}
			
		}
		
		
		// if a pawn has reached the other end of the board, we need to promote it
		// in addition to moving it.
		if (movedPiece.type == PieceType.PAWN && promotePawn(movedPiece, to)) {
			
			Piece newQueen = new Piece(PieceType.QUEEN, movedPiece.color);
			
			b.removePiece(movedPiece, from);
			b.placePiece(newQueen, to);			
			
			performedMove.promotedPawn = true;

			
		// If we don't need to promote a pawn, then we can just update the piece's position
		// on the board and on the lists.
		} else {
			b.removePiece(movedPiece, from);
			b.placePiece(movedPiece, to);
		}
		
		// update the performed moves stack
		performedMove.move = m;
		b.performedMoves.add(performedMove);
		//System.out.println("Push:" + b.performedMoves.size());
		
		// update game progress information
		if (b.turn == Color.WHITE) {
			b.turn = Color.BLACK;
		} else {
			b.turn = Color.WHITE;
			b.moves += 1;
		}
		
	}
	
	/**
	 * Undoes the latest move. 
	 * @return
	 */
	public Board undo() {
		
		if (this.performedMoves.isEmpty())
			return this;
		
		PerformedMove moveToUndo = this.performedMoves.get(performedMoves.size()-1);
		this.performedMoves.remove(performedMoves.size()-1);
		
		Square from = moveToUndo.move.getTo();
		Square to = moveToUndo.move.getFrom();
		Piece movedPiece = this.board[from.row][from.col];
		Piece capturedPiece = moveToUndo.capturedPiece; 
				
		// deprecate if promoted
		if (moveToUndo.promotedPawn) {
			Piece newPawn = new Piece(PieceType.PAWN, movedPiece.color);
			
			this.removePiece(movedPiece, from);
			this.placePiece(newPawn, to);			
			
		} else {

			this.removePiece(movedPiece, from);
			this.placePiece(movedPiece, to);		
		}
		
		// if a piece was captured, put it back.
		if (capturedPiece != null) {
			this.placePiece(capturedPiece, from);
		}
		
		// update game progress information
		if (this.turn == Color.WHITE) {
			this.turn = Color.BLACK;
			this.moves -= 1;
		} else {
			this.turn = Color.WHITE;
		}
		
		// reset these to false just to be sure.
		this.gameOver = false;
		this.resigned = false;
		
		return this;
		
	}

	
	private void removePiece(Piece p, Square s) {
		this.board[s.row][s.col] = null;
		this.pieces.remove(p);
		if(p.color == Color.BLACK) {
			this.blackPieces.remove(p);
		} else {
			this.whitePieces.remove(p);
		}
		// update hash
		this.hash ^= this.hashfac.getHashValue(p, s);
		// update score
		if (p.color == Color.BLACK)
			this.score += ScoreFactory.scorePiece(p, s);
		else 
			this.score -= ScoreFactory.scorePiece(p, s);
	}
	
	private void placePiece(Piece p, Square s) {
		this.board[s.row][s.col] = p;
		this.pieces.put(p, s);
		if(p.color == Color.BLACK) {
			this.blackPieces.put(p, s);
		} else {
			this.whitePieces.put(p, s);
		}
		// update hash		
		this.hash ^= this.hashfac.getHashValue(p, s);
		// update score
		if (p.color == Color.BLACK)
			this.score -= ScoreFactory.scorePiece(p, s);
		else 
			this.score += ScoreFactory.scorePiece(p, s);
	}
	
	boolean isGameOver() {
		return gameOver;
	}
	

}
